Skip to content

Interface type management: admin CRUD, lifecycle, junctions, merge#5

Merged
ndemarco merged 10 commits intomainfrom
logo-integration
Apr 19, 2026
Merged

Interface type management: admin CRUD, lifecycle, junctions, merge#5
ndemarco merged 10 commits intomainfrom
logo-integration

Conversation

@ndemarco
Copy link
Copy Markdown
Owner

@ndemarco ndemarco commented Apr 19, 2026

Summary

Introduces a full admin surface for interface types — the named compatibility classes that govern insert ↔ receptacle placement. Seven commits, four implementation slices plus spec/prototype, green across 407 tests.

Prior to this PR, interface types were a thin interface_types table and single-text columns on template_versions, locations, and inserts. After this PR:

  • Interface types carry maturity (draft | stable), archived_at, and unit_system (per-axis modular convenience units). Identifier is a mutable display slug; all references are by UUID.
  • Membership moves to three junction tables: template_version_interfaces_{provided,accepted} and location_interfaces_accepted. Junctions support multi-membership per spec (e.g., an Akro-Mils bin providing both louver-hang and open-surface).
  • A merge operation consolidates duplicate types into a survivor in one transaction — junction rewrite + template-version minting + hard-delete of sources.
  • New /admin/interfaces page provides list, filter tabs (active/archived/all), detail form with per-axis unit editor, lifecycle menu (archive/unarchive/delete with gate), and a bulk merge modal with survivor picker.
  • Consumer pages (/inserts, /modules, /modules/new, /modules/<id>) updated to read/write the new array shape.

(Branch is named logo-integration for historical reasons — the logo commit itself already merged to main via a separate path; every commit here is interface-type work.)

Spec

Full design at specification/interface-type-management.md. Built via WhereTF's 5-phase UX dev pattern (specify → survey → converge → prototype → implement), with prototype at prototypes/interfaces-v1.html.

Commits

  1. dc182c3 — spec + prototype v1
  2. 08103e4 — prototype maturity UX simplification (drop form radio, defer to save button)
  3. 16797feslice 1: lifecycle columns + junction tables + archive/unarchive + one-way maturity guard
  4. f952972slice 2: repos migrated onto junctions, old text columns dropped (backfill in migration 0014)
  5. e835649slice 4: /admin/interfaces list + detail + CRUD UI
  6. 317bc85slice 3: merge repo + API + UI
  7. 6943295 — consumer pages updated to new shape

Key design decisions

  • Stable is terminal. Maturity is a one-directional state machine (draft → stable → archived → deleted). No demotion path. If a stable type is wrong, rename/merge/archive it.
  • Junction rows by UUID, never identifier. Identifier renaming is safe by construction. The schema file carries a load-bearing-invariant comment to protect this.
  • Inserts inherit provided interfaces from their template. No per-insert override.
  • Merge bypasses the archive-gate. Hard-deletes sources as part of the consolidation transaction. Mints a new template version per affected template because interfaces are versioned properties per spec.
  • Placement is optimistic. Interface-match check is set intersection; either side empty = allow. "User rules the storage."
  • Unit system is a display convenience, not authoritative geometry. SI mm stays the storage truth; the interface just provides per-axis input units.

Schema changes

Two new migrations. Both idempotent; apply cleanly to a fresh DB.

  • 0013_interface_type_lifecycle.sql — adds maturity, archived_at, unit_system columns and the three junction tables.
  • 0014_interface_type_junctions_migrate.sql — backfills the junction tables from the old text columns, then drops them along with the deprecated template_versions.unit_size.

No insert_interfaces_provided table; inserts inherit from template.

Deployment notes

  • Dev DB bookkeeping drift fixed. drizzle.__drizzle_migrations was only tracking 0000-0005 pre-PR; 0006-0014 were in the DB but unrecorded. Backfilled on dev + test DBs, so drizzle-kit migrate now runs cleanly.
  • On a fresh prod DB, all 15 migrations apply normally via the migrator image. No manual steps.
  • On a prod DB carrying the same bookkeeping drift, the migrator will hang. Backfill drizzle.__drizzle_migrations with the missing hashes before running the deploy.

Test plan

  • 407 tests passing (50 interface-type repo tests + 357 existing, all green)
  • Smoke-tested all four admin routes — /admin/interfaces list, detail, create, edit, archive, unarchive, delete, merge modal
  • End-to-end curl test of the lifecycle (create → promote → demote-409 → delete-active-409 → archive → delete-200)
  • End-to-end curl test of merge (multi-source → target, refs rewritten, sources deleted, 409 on target-in-sources, 404 on missing target)
  • All four consumer pages return 200 and receive the new shape
  • Manual UI walkthrough on staging (admin CRUD, merge, module creation with interface selection, insert placement)
  • Deploy migrator, verify both migrations apply exit-0 on the prod DB

Known follow-ups (not blocking this PR)

  • Template editor chip input (provided/accepted multi-select) — spec §"Template Editor Integration". Templates can only be given interfaces via seed or API today.
  • Level editor chip input (multi-accept). Level form is single-select for now.
  • Dimensional fit + overflow check at placement time. Interface-match-only today.
  • Cross-location overflow helper (locationNeighborsRepository.getAdjacentInDirection) — spec §"Cross-location overflow helper".
  • isAdmin({ userId }) helper shape — API routes gate nothing today. Stub for when multi-tenant auth lands.
  • Undo on lifecycle toasts — toasts are informational only right now.

NickyDoes added 8 commits April 17, 2026 23:09
Restore the WhereTF map pin logo to the sidebar header, add it as
an SVG favicon (both app/icon.svg route and public/ static fallback),
and replace all plain "Loading..." text with an animated pin spinner.

Remove unused default Next.js boilerplate SVGs from public/.
Fix stale .env.local.example (was referencing MongoDB/OAuth).
Spec covers admin CRUD, template+receptacle integration, placement
check (interface match + dimensional fit), lifecycle (archive/merge/
delete), maturity states, and optional unit-system convenience layer.

Prototype shows list view with filter tabs, bulk merge flow, detail
panel with per-axis unit editor, archive/delete lifecycle menu, and
chip preview.
Maturity radio in the edit form was too prominent. Remove it entirely.
Default new interfaces to stable; "Save as draft" secondary button is
the explicit opt-in to draft. Edit form for stable types has no
demotion path — state machine is one-way (draft → stable → archived).

Detail header shows a small draft pill when viewing a draft.
Slice 1 of interface-type-management implementation — data layer only,
no UI yet. Extends interface_types with maturity (draft|stable),
archived_at timestamp, and unit_system jsonb. Adds junction tables for
template_version × interface_type (provided + accepted) and
location × interface_type (accepted). Old single-text columns stay in
place and authoritative for now; slice 2 migrates template/location
repos onto the junctions.

Repository gains archive / unarchive / usageCount. Update enforces
one-way maturity state machine — stable is terminal. Remove requires
archived + zero usage. List supports status=active|archived|all.

API: new POST /api/interface-types/:id/archive and /unarchive routes.
Existing PATCH returns 409 on stable→draft demotion. DELETE returns
409 until archived and unused. GET detail includes usage counts.

40 repo tests pass; end-to-end smoke test via curl covers the full
lifecycle.
Slice 2 of interface-type-management. Single-text columns on template
versions, locations, and inserts are replaced by UUID-keyed junction
tables. Backfill resolves identifiers to UUIDs in migration 0014 then
drops the old columns. template_versions.unit_size (the "42mm" text
field) also drops — unit info now lives on interface_types.unit_system.

Repositories now take UUID arrays for interface membership:
  templateRepository.create / publishVersion accept
    interfacesProvidedIds[] and interfacesAcceptedIds[]
  locationRepository.create accepts interfacesAcceptedIds[]
  insertRepository drops the interfaceTypeProvided param — inserts
    inherit provided interfaces from their template version per spec
    (no per-insert override)

New helpers: templateRepository.getVersionInterfaces /
setVersionInterfaces, locationRepository.getAcceptedInterfaces /
setAcceptedInterfaces / getAcceptedInterfacesByLocationIds.

Placement check is now set intersection over UUIDs. Still optimistic
when either side is empty (user rules the storage).

API: /api/locations GETs now include interfacesAccepted on each
location; PATCH accepts interfacesAcceptedIds to drive
setAcceptedInterfaces. /api/inserts list filter renamed to
interfaceTypeId (UUID) and listWithDetails returns
interfaceTypes: [{ id, identifier }].

Seed rewritten to use repo helpers and pass UUIDs. Full test suite
passes (397 tests).

Pages (app/inserts, app/modules) still reference the old single-string
shape and will get stale data until slice 4 rebuilds the chip UI.
Slice 4 of interface-type-management. New route /admin/interfaces is
the admin surface for managing interface types. List view on the left
(checkbox + identifier + maturity badge + status badge + description +
usage counts + created date + row actions) with Active / Archived /
All filter tabs and a bulk-actions bar. Detail pane on the right opens
on row click or the "New Interface Type" header button.

Detail form mirrors the prototype: identifier (mutable slug), multi-
line description, unit-system toggle with per-axis label + mm input,
free-form physical-contract notes, live usage panel. Derive-new button
clones description + contract + unit system into a fresh draft.

Save button behavior follows the spec state machine:
  Create mode: "Save as draft" + "Create" (stable default)
  Edit draft: "Save as draft" + "Save" (promotes to stable)
  Edit stable: single "Save" — no demotion path

Lifecycle menu in the header handles Archive / Unarchive / Delete.
Delete is gated on archived + 0 usage; the menu surfaces the reason
when disabled. Archive row action mirrors the same logic in-line.

Sidebar gains an Interfaces entry under the Admin section so the
route is discoverable. Merge (bulk action) is stubbed with a
disabled button — lands in the final slice alongside its backend.
Final slice of interface-type-management. The merge operation
consolidates N source interface types into a single target, in one
transaction:

  1. Rewrite template_version_interfaces_{provided,accepted} junctions
     from source IDs to target ID, deduping on conflict so a template
     version that already holds target doesn't get a duplicate row.
  2. Rewrite location_interfaces_accepted the same way.
  3. Mint a new template version per affected template, cloning the
     latest affected version's structure and carrying the now-remapped
     junctions forward — interfaces-provided/accepted are versioned
     properties per spec, so a merge must be a versioned event.
  4. Hard-delete the source interface_types rows (merge bypasses the
     archive-gate; tombstoning is part of the consolidation).

Errors: empty sourceIds (400), target in sources (409), target or
source not found (404).

API: POST /api/interface-types/merge with { sourceIds, targetId }
returns { referencesUpdated, templateVersionsMinted, sourcesDeleted }.

Admin page: "Merge into…" bulk action opens a modal showing the
selected sources, total reference count, and a radio-pick survivor
list (active non-selected types, with usage ref counts). Destructive
warning surfaces the ref + version mint counts. Execute refreshes the
list, closes the detail pane if a source was active, and toasts the
summary.

Interface-type-management work is now complete (slices 1–4).
50 repo tests pass; smoke-tested end-to-end via curl.
Slice 2 dropped the single-text interface_type columns and moved
membership to junction tables, but the /inserts, /modules, and
/modules/new pages were still reading ins.interfaceType (string) and
level.interfaceTypeAccepted (string) and sending those names back in
mutation bodies. The fields no longer exist, so data was stale and
mutations silently dropped.

All four pages now use the new shape:
  - Location.interfacesAccepted: Array<{ id, identifier }>
  - Insert.interfaceTypes: Array<{ id, identifier }>
  - Level form state switches from interfaceTypeAccepted: string to
    interfaceTypeId: string (single-select UUID; multi-select chips
    are a later slice)
  - Query params and PATCH/POST bodies use interfaceTypeId /
    interfacesAcceptedIds (UUIDs), never identifier strings
  - Dropdowns key by UUID id, display identifier
  - Chip rendering joins identifiers when a level accepts or an
    insert provides multiple

templateRepository.listWithCurrentVersion now batch-loads provided
and accepted interface identifiers onto currentVersionData so the
module page can filter candidate templates against a receptacle's
accepted interface without a second round-trip.

No schema change. All 407 tests pass. Smoke-tested /modules,
/modules/new, /modules/<id>, /inserts, /admin/interfaces — all return
200 and the API responses carry the new array shape.
@ndemarco ndemarco closed this Apr 19, 2026
@ndemarco ndemarco reopened this Apr 19, 2026
@ndemarco ndemarco changed the title Logo integration Interface type management: admin CRUD, lifecycle, junctions, merge Apr 19, 2026
NickyDoes and others added 2 commits April 18, 2026 21:01
TS2322: setDraft is SetStateAction<FormDraft | null>, but the
DetailForm prop declared the narrower (FormDraft | (FormDraft) =>
FormDraft) => void, which an updater that returns null couldn't
satisfy. Use React's SetStateAction<FormDraft | null> directly so
the prop signature matches the useState setter exactly.
@ndemarco ndemarco merged commit b7c01f2 into main Apr 19, 2026
4 checks passed
@ndemarco ndemarco deleted the logo-integration branch April 19, 2026 01:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant